#!/usr/bin/python3
# -*- coding: utf-8 -*-

from collections import Iterable
from glob import iglob
from os import unlink, makedirs, name as os_name
from os.path import normpath, relpath, join as joinpath, isfile, isdir, dirname, basename
from re import compile as compile_regex, DOTALL, UNICODE
from shutil import copy, copytree, rmtree

from .callbacks import CallbackLogger
from .command import Environment, Command, ExecuteCommand
from .validators import reject_if_blank, reject_if_null

_WILDCARDS = compile_regex('[*?[\\]]', DOTALL | UNICODE)


def scanfiles(
        root: str,
        includes: tuple,
        excludes: tuple
):
    """ 扫描文件集合。

    :param root:
    :param includes:
    :param excludes:
    :return:
    """
    from os import walk
    from fnmatch import fnmatch

    if not isdir(root):
        raise ValueError(f'{root} is not a directory')

    for dirpath, dirnames, filenames in walk(root):
        rd = relpath(dirpath, root)
        for filename in filenames:
            rf = joinpath(rd, filename)
            excluded = any(fnmatch(rf, p) for p in excludes)
            if excluded:
                continue
            included = any(fnmatch(rf, p) for p in includes)
            if included:
                yield normpath(joinpath(root, rf)), normpath(rf)


class CopyCommand(Command):
    """ 用于执行复制的命令。

    可以复制单个文件，并指定目标文件名，或者复制一批文件到指定的目录中。
    - 如果`src`是单个文件，并且`dst`是已存在的目录，那么复制该文件到指定目录下。
    - 如果`src`是单个文件，并且`dst`是已存在的文件，那么复制该文件到`dst`并覆盖。
    - 如果`src`是单个文件，并且`dst`不存在，那么复制该文件到目标文件，使用`dst`作为目标文件名。
    - 如果`src`是单个目录，那么复制该目录树到目标目录下，使用`dst` + basename(`src`)作为目标目录名。
    - 如果`src`存在通配符，那么使用该通配符匹配一批文件，如果`dst`是已存在的目录，那么复制这批文件到指定目录下。否则创建`dst`目录，然后复制。如果创建失败则抛出错误。
    - 否则抛出错误。
    """
    __slots__ = ('__src', '__dst',)

    def __init__(self,
                 src: str,
                 dst: str,
                 description: str = None,
                 check_required: bool = False
                 ):
        """ 构造复制命令。

        :param src: 源文件。
        :param dst: 目标文件。
        """
        super().__init__(description, check_required)

        reject_if_blank(src)
        reject_if_blank(dst)

        self.__src = src
        self.__dst = dst

    @property
    def src(self):
        return self.__src

    @property
    def dst(self):
        return self.__dst

    def __call__(
            self,
            environment: Environment,
            resources: list,
            callback: CallbackLogger
    ):
        reject_if_null(environment)
        reject_if_null(resources)

        def make_dst_dir(dd):
            if isfile(dd):
                raise RuntimeError('cannot make directory, same name file exists: ' + dd)
            elif isdir(dd):
                pass
            else:
                callback.info('make non-exists directory {d}', d=dd)
                makedirs(dd, exist_ok=True)

        def rm_dst_dir(dd):
            if isfile(dd):
                unlink(dd)
            elif isdir(dd):
                callback.info('remove exists directory {d}', d=dd)
                rmtree(dd, ignore_errors=True)
            else:
                pass

        src = self.abspath(self.interpolate(self.src, environment), environment)
        dst = self.abspath(self.interpolate(self.dst, environment), environment)

        if isfile(src):
            if isdir(dst):
                callback.info('copy {src} to directory {dst}', src=src, dst=dst)
                copy(src, dst)
            elif isfile(dst):
                callback.info('copy {src} to {dst}', src=src, dst=dst)
                copy(src, dst)
            else:
                callback.info('copy {src} to {dst}', src=src, dst=dst)
                make_dst_dir(dirname(dst))
                copy(src, dst)

            if self.check_required:
                if not isfile(dst):
                    raise RuntimeError('cannot copy file to ' + dst)
        elif isdir(src):
            make_dst_dir(dst)

            df = joinpath(dst, basename(src))
            rm_dst_dir(df)
            callback.info('copy tree {src} to directory {dst}', src=src, dst=df)
            copytree(src, df)

            if self.check_required:
                if not isdir(dst):
                    raise RuntimeError('cannot copy directory to ' + dst)
        elif _WILDCARDS.match(src):
            make_dst_dir(dst)

            for f in iglob(src, recursive=True):
                if isfile(f):
                    callback.info('copy {src} to directory {dst}', src=f, dst=dst)
                    copy(f, dst)
                elif isdir(f):
                    df = joinpath(dst, basename(f))
                    rm_dst_dir(df)
                    callback.info('copy tree {src} to directory {dst}', src=f, dst=df)
                    copytree(f, df)
        else:
            callback.warn('cannot find {src}', src=src)

        return None


class RemoveCommand(Command):
    """ 用于执行删除的命令。
    可以删除文件或者目录，删除目录时会删除所有的子级内容。
    """
    __slots__ = ('__file',)

    def __init__(self,
                 file: str,
                 description: str = '',
                 check_required: bool = False
                 ):
        """ 构造删除命令。
        :param: file 待删除文件或者目录，可以包含通配符。
        """
        super().__init__(description, check_required)

        reject_if_null(file)

        self.__file = file

    @property
    def file(self):
        return self.__file

    def __call__(
            self,
            environment: Environment,
            resources: list,
            callback: CallbackLogger
    ):
        reject_if_null(environment)
        reject_if_null(resources)

        file = self.abspath(self.interpolate(self.file, environment), environment)

        if isdir(file):
            callback.info('remove directory {src}', src=file)
            rmtree(file, ignore_errors=True)

            if self.check_required:
                if isdir(file):
                    raise RuntimeError('cannot remove directory ' + file)
        elif isfile(file):
            callback.info('unlink file {src}', src=file)
            unlink(file)

            if self.check_required:
                if isfile(file):
                    raise RuntimeError('cannot unlink ' + file)
        elif _WILDCARDS.match(file):
            for f in iglob(file, recursive=True):
                if isdir(f):
                    callback.info('remove directory {src}', src=f)
                    rmtree(f, ignore_errors=True)
                elif isfile(f):
                    callback.info('unlink file {src}', src=f)
                    unlink(f)
        else:
            pass

        return None


class LocalExecuteCommand(ExecuteCommand):
    """ 用于执行本地系统命令的命令。

    """
    __slots__ = ('__command_lines', '__redirect_encoding', '__timeout')

    __DEFAULT_TIMEOUT = 15

    def __init__(self,
                 command_lines: (str, Iterable),
                 redirect_encoding: str = None,
                 timeout: int = None,
                 description: str = '',
                 check_required: bool = False
                 ):
        super().__init__(command_lines, redirect_encoding, timeout, description, check_required)

    def call_single(
            self,
            line: str,
            environment: Environment,
            resources: list,
            callback: CallbackLogger
    ):
        from shlex import split as splitargs
        from subprocess import Popen, TimeoutExpired, PIPE, DEVNULL

        reject_if_blank(line)

        callback.info('{line}', line=line)

        args = splitargs(line, posix=os_name != 'nt')
        with Popen(args, stdin=DEVNULL, stdout=PIPE, stderr=PIPE, cwd=self.abspath('', environment),
                   universal_newlines=True,
                   encoding=self.redirect_encoding, errors=self.redirect_encoding) as proc:
            try:
                return_code = proc.wait(self.timeout)
            except TimeoutExpired:
                callback.error('sub process timeout, positive killed. {err}')
                proc.kill()
                return -1
            except AttributeError:
                callback.error('unexpected error. {err}')
                proc.kill()
                return -2

            for t in proc.stdout:
                callback.verbose('>> {t}', t=t.strip())
            for t in proc.stderr:
                callback.error('>> {t}', t=t.strip())

            return return_code
